{
 "metadata": {
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": 3
  },
  "orig_nbformat": 2
 },
 "nbformat": 4,
 "nbformat_minor": 2,
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "Kwiver Processes in Python\r\n",
    "==========================\r\n",
    "\r\n",
    "Note: this file pertains only to the implementation of a Sprokit process in python, and does not directly cover pipelines or any related topics. That is the subject of another doc.\r\n",
    "\r\n",
    "Algos and Arrows can be utilzied programmatically directly from a python script, or composed as components of a pipeline integrating any combination of C++ and Python based processes, utilizing the vital types to ensure universal data compatibility. To this end, if implementing a mixed pipeline with C++ and Python processes that leverages a custom, non vital type, either in C++ or Python, a Pybind11 binding must be created for the type and exposed to the pipeline so that python/C++ can interpret and properly use the type\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Python Processes can inherit from one of two base classes that support fundamental process methods, KwiverProcess and PythonProcess. Below are examples of each approach."
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "A further note that if a process exists in C++ it does *not* need to be bound in Python as C++ processes can be incorporated into mixed pipelines alongside python processes out of the box. This includes pipelines executed from the python interpreter."
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "from kwiver.sprokit.pipeline import process, datum\r\n",
    "from kwiver.kwiver_process import KwiverProcess\r\n",
    "\r\n",
    "\r\n",
    "class HelloWorldPython(process.PythonProcess):\r\n",
    "    # process instances need to expose the config obj as a constructor argument\r\n",
    "    # if a process for whatever reason does not require/need a conf object\r\n",
    "    # A default arg can be used. This will be rare.\r\n",
    "    def __init__(self, conf = None):\r\n",
    "        super().__init__(self,conf)\r\n",
    "\r\n",
    "class HelloWorldPythonKw(process.KwiverProcess):\r\n",
    "    def __init__(self, conf = None):\r\n",
    "        super().__init__(self,conf)      \r\n",
    "\r\n"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Much like algos, we also need to expose this process to the pipeline runner via a dunder registration method"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "def __sprokit_register__():\r\n",
    "    from kwiver.sprokit.pipeline import process_factory\r\n",
    "\r\n",
    "    module_name = 'python:kwiver.hello_world'\r\n",
    "\r\n",
    "    if process_factory.is_process_module_loaded(module_name):\r\n",
    "        return\r\n",
    "\r\n",
    "    process_factory.add_process('hello_world_process',\r\n",
    "                                'A Simple Kwiver Test Process',\r\n",
    "                                HelloWorldPython)\r\n",
    "\r\n",
    "    process_factory.mark_process_module_as_loaded(module_name)"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Of course, in its current state, this process does absolutely nothing, and is a fairly trivial example. The following examples demonstrate a little more diverse functionality.\n",
    "\n",
    "To properly implement a process we need to implement a couple of core methods, without which, the process will not function. These functions are the ```_configure``` step and the ```_step``` step. The configure step serves to construct the process during exeuction of a pipeline and the step is the programatic exeuction of the process.\n",
    "\n",
    "This process serves as the only node in the pipeline, accepting and producing no output ports. A more substantive example will follow."
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "import sys\r\n",
    "\r\n",
    "class HelloWorldPython(process.KwiverProcess):\r\n",
    "    def __init__(self, conf):\r\n",
    "        super().__init__(self,conf)\r\n",
    "        flags = kwiver.sprokit.pipeline.process.PortFlags()\r\n",
    "        flags.add(self.flag_required)\r\n",
    "    def _configure(self):\r\n",
    "        pass\r\n",
    "    def _step(self):\r\n",
    "        sys.stdout.write(\"hello world\")\r\n",
    "        self._base_step()"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\\# Pipefile for this process\n",
    "\n",
    "#==================================================================\n",
    "\n",
    "process helloworld\n",
    "\n",
    "  :: hello_world_process\n",
    "\n",
    "#==================================================================\n",
    "\n",
    "config _scheduler\n",
    "\n",
    "  type = pythread_per_process\n",
    "\n",
    "\\# -- end of file --"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Next we will detail the differentiation between inheriting from `process.KwiverProcess` and `process.PythonProcess`"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Pure python process implementing functionality from python\n",
    "\n",
    "For this example we will be implementing a *very* simple 3d point class"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "import numpy as np\r\n",
    "\r\n",
    "class pt3d(object):\r\n",
    "    def __init__(self, x = 0, y = 0, z = 0, dtype = np.float32):\r\n",
    "        self.x = x\r\n",
    "        self.y = y\r\n",
    "        self.z = z\r\n",
    "        self.dtype = dtype\r\n",
    "    def as_array(self):\r\n",
    "        return np.ndarray((1,3), dtype=self.dtype, buffer=[self.x,self.y,self.z])\r\n",
    "    def as_mat(self):\r\n",
    "        return np.matrix(self.as_array())\r\n",
    "    def __str__(self):\r\n",
    "        return \"(%s)\" % \",\".join(str(self.x), str(self.y), str(self.z))"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "Now that we have a basic data type, we need to create processes that can handle this datatype. Preexisting Kwiver processes will not support it as this data type is outside of vital. For demonstration purposes, the KwiverProcess utility mixin will be used.\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "from kwiver.sprokit.processes import KwiverProcess\r\n",
    "\r\n",
    "\r\n",
    "class NodeSource(KwiverProcess):\r\n",
    "    # Note we need to have an option for the config in our constructor\r\n",
    "    def __init__(self, conf):\r\n",
    "        super().__init__(self, conf)\r\n",
    "        required = process.PortFlags()\r\n",
    "        required.add(self.flag_required)\r\n",
    "        self.add_type_trait(\r\n",
    "            \"pt3d\",\r\n",
    "            \"pt3d\",\r\n",
    "            datum.Datum.get_datum,\r\n",
    "            datum.new\r\n",
    "        )\r\n",
    "        self.add_port_trait(\"pt3d\",\"pt3d\",\"Custom numpy 3d pt class\")\r\n",
    "        self.declare_output_port_using_trait(\"3dpt\", required)\r\n",
    "    def _configure(self):\r\n",
    "        self._base_configure()\r\n",
    "        self.data = pt3d(2,3,4,np.int64)\r\n",
    "    def _step(self):\r\n",
    "        self.push_to_port_using_trait(\"pt3d\", self.data)\r\n",
    "        self.push_to_port_using_trait(\"pt3d\", datum.complete())\r\n",
    "        self.mark_process_as_complete()\r\n",
    "\r\n",
    "def __sprokit_register__():\r\n",
    "    from sprokit.pipeline import process_factory\r\n",
    "    module_name = \"python:source\"\r\n",
    "    if process_factory.is_process_module_loaded(module_name):\r\n",
    "        return\r\n",
    "    process_factory.add_process(\"SourceNode\",\"3dpt producer\", NodeSource)\r\n",
    "    process_factory.mark_process_module_as_loaded(module_name)\r\n"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "from kwiver.sprokit.processes import KwiverProcess\r\n",
    "from kwiver.vital.types import Point3d\r\n",
    "class Node(KwiverProcess):\r\n",
    "    def __init__(self, conf):\r\n",
    "        super().__init__(self, conf)\r\n",
    "        required = process.PortFlags()\r\n",
    "        required.add(self.flag_required)\r\n",
    "        self.add_type_trait(\r\n",
    "            \"pt3d\",\r\n",
    "            \"pt3d\",\r\n",
    "            datum.Datum.get_datum,\r\n",
    "            datum.new\r\n",
    "        )\r\n",
    "        self.add_type_trait(\r\n",
    "            \"point\",\r\n",
    "            \"kwiver:point\",\r\n",
    "            datum.Datum.get_datum,\r\n",
    "            datum.new\r\n",
    "        )\r\n",
    "        self.add_port_trait(\"pt3d\",\"pt3d\",\"Custom numpy 3d pt class\")\r\n",
    "        self.add_port_trait(\"point\", \"point\", \"3d point in world frame\")\r\n",
    "        self.declare_input_port_using_trait(\"3dpt\",required)\r\n",
    "        self.declare_output_port_using_trait(\"point\",required)\r\n",
    "    def _configure(self):\r\n",
    "        self._base_configure()\r\n",
    "    def _step(self):\r\n",
    "        pt3d = self.grab_input_using_trait(\"pt3d\")\r\n",
    "        pt_mat = pt3d.as_mat()\r\n",
    "        vital_pt = Point3d(pt_mat)\r\n",
    "        self.push_to_port_using_trait(\"point\")\r\n",
    "\r\n",
    "def __sprokit_register__():\r\n",
    "    from sprokit.pipeline import process_factory\r\n",
    "    module_name = \"python:source\"\r\n",
    "    if process_factory.is_process_module_loaded(module_name):\r\n",
    "        return\r\n",
    "    process_factory.add_process(\"SourceNode\",\"3dpt producer\", NodeSource)\r\n",
    "    process_factory.mark_process_module_as_loaded(module_name)"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "from kwiver.sprokit.process import KwiverProcess\r\n",
    "from kwiver.vital.types import Point3d\r\n",
    "\r\n",
    "class NodeSink(KwiverProcess):\r\n",
    "    def __init__(self, conf):\r\n",
    "        super().__init__(self, conf)\r\n",
    "        required = process.PortFlags()\r\n",
    "        required.add(self.flag_required)\r\n",
    "        self.add_type_trait(\r\n",
    "            \"point\",\r\n",
    "            \"kwiver:point\",\r\n",
    "            datum.Datum.get_datum,\r\n",
    "            datum.new\r\n",
    "        )\r\n",
    "        self.add_port_trait(\"point\",\"point\",\"3d point in world plane\")\r\n",
    "        self.declare_input_port_using_trait(\"point\",required)\r\n",
    "    def _configure(self):\r\n",
    "        self._base_configure()\r\n",
    "    def _step(self):\r\n",
    "        pt = self.grab_input_using_trait('point')\r\n",
    "        print(pt)\r\n",
    "def __sprokit_register__():\r\n",
    "    from sprokit.pipeline import process_factory\r\n",
    "    module_name = \"python:sink\"\r\n",
    "    if process_factory.is_process_module_loaded(module_name):\r\n",
    "        return\r\n",
    "    process_factory.add_process(\"SinkNode\",\"3d pt producer\", NodeSink)\r\n",
    "    process_factory.mark_process_module_as_loaded(module_name)"
   ],
   "outputs": [],
   "metadata": {}
  }
 ]
}